/*
* Copyright 2012 Brendan McCarthy (brendan@oddsoftware.net)
*
* This file is part of Feedscribe.
*
* Feedscribe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3
* as published by the Free Software Foundation.
*
* Feedscribe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Feedscribe. If not, see <http://www.gnu.org/licenses/>.
*/
package net.oddsoftware.android.feedscribe.data;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.TimeZone;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.ContentNode;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.HtmlNode;
import org.htmlcleaner.SpecialEntity;
import org.htmlcleaner.TagNode;
import org.htmlcleaner.TagNodeVisitor;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Environment;
import net.oddsoftware.android.feedscribe.*;
import net.oddsoftware.android.html.HttpCache;
import net.oddsoftware.android.utils.Logger;
import net.oddsoftware.android.utils.Utilities;
public class FeedManager {
private static FeedManager mInstance = null;
private FeedDBAdaptor mDB;
int mPackageVersion;
int mPreviousPackageVersion;
public static String USER_AGENT = "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62)" +
" AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17";
private FeedUpdateListener mFeedUpdateListener = null;
private ArrayList<Download> mDownloads;
private FeedConfig mFeedConfig = null;
private Logger mLog = null;
public static final SimpleDateFormat rfc822DateFormats[] = new SimpleDateFormat[]
{
// Sat, 22 Jan 2011 19:25:00 +1100
new SimpleDateFormat("EEE, d MMM yy HH:mm:ss z", Locale.US),
new SimpleDateFormat("EEE, d MMM yy HH:mm z", Locale.US),
new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US),
new SimpleDateFormat("EEE, d MMM yyyy HH:mm z", Locale.US),
new SimpleDateFormat("d MMM yy HH:mm z", Locale.US),
new SimpleDateFormat("d MMM yy HH:mm:ss z", Locale.US),
new SimpleDateFormat("d MMM yyyy HH:mm z", Locale.US),
new SimpleDateFormat("d MMM yyyy HH:mm:ss z", Locale.US),
new SimpleDateFormat("d MMM yyyy HH:mm:ss", Locale.US),
// Here is an example of an invalid RFC822 date-time. This is commonly seen in RSS 1.0 feeds generated by older versions of Movable Type:
// 2002-10-02T08:00:00-05:00
//new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz"),
};
public static final int MAX_ETAG_LENGTH = 200;
public synchronized static FeedManager getInstance(Context ctx)
{
if( mInstance == null )
{
mInstance = new FeedManager(ctx);
}
return mInstance;
}
public synchronized static void closeInstance()
{
if( mInstance != null )
{
mInstance.close();
mInstance = null;
}
}
protected FeedManager(Context ctx)
{
mDB = new FeedDBAdaptor(ctx);
mDB.open();
mFeedConfig = FeedConfig.getInstance(ctx);
mLog = Globals.LOG;
// try and pull the package version from the package manager
mPackageVersion = Globals.VERSION_CODE;
mPreviousPackageVersion = 0;
try
{
PackageInfo info = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0);
mPackageVersion= info.versionCode;
}
catch(NameNotFoundException exc)
{
if (mLog.e()) mLog.e("unable to get package version ", exc);
}
mDownloads = new ArrayList<Download>();
loadDownloads();
loadConfig();
}
public void close()
{
mDB.close();
}
public ArrayList<ShortFeedItem> getShortItems(int feedTypes)
{
ArrayList<ShortFeedItem> result = new ArrayList<ShortFeedItem>();
ArrayList<Feed> feeds = mDB.getFeeds(feedTypes);
for(Feed feed: feeds)
{
ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feed.mId, null, false);
result.addAll(items);
}
Collections.sort(result);
Collections.reverse(result);
return result;
}
public ArrayList<ShortFeedItem> getShortItems(int feedTypes, String query)
{
String[] terms = query.split("\\s");
ArrayList<ShortFeedItem> result = new ArrayList<ShortFeedItem>();
ArrayList<Feed> feeds = mDB.getFeeds(feedTypes);
for(Feed feed: feeds)
{
ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feed.mId, terms, false);
result.addAll(items);
}
Collections.sort(result);
Collections.reverse(result);
return result;
}
public ArrayList<ShortFeedItem> getShortItems(long feedId)
{
ArrayList<ShortFeedItem> result = mDB.getShortFeedItems(feedId, null, false);
if( result != null )
{
Collections.sort(result);
Collections.reverse(result);
}
return result;
}
public void deDuplicate(ArrayList<ShortFeedItem> items)
{
int i = 1;
while( i < items.size() )
{
ShortFeedItem cur = items.get(i);
ShortFeedItem prev = items.get(i-1);
boolean remove = false;
if( cur.mLink.equals( prev.mLink ) )
{
remove = true;
}
else if( cur.mTitle.equals( prev.mTitle ) )
{
try
{
URI currentURI = new URI(cur.mLink);
URI prevURI = new URI(prev.mLink);
if( currentURI.getHost().equals(prevURI.getHost()) )
{
if (mLog.d()) mLog.d("removing duplicate from display because of title and host match");
remove = true;
}
}
catch(URISyntaxException exc)
{
if (mLog.e()) mLog.e("deDuplicateArray error parsing links", exc);
}
}
if( remove )
{
if (mLog.d()) mLog.d("removing duplicate from display, link is " + cur.mLink);
items.remove(i);
}
else
{
++i;
}
}
}
public FeedItem getItemById(long id)
{
return mDB.getFeedItem(id);
}
public boolean updateItem(FeedItem item)
{
return mDB.updateFeedItem(item);
}
public boolean updateItemFlags(FeedItem item)
{
return mDB.updateFeedItemFlags(item);
}
/** @return true if any updates were attempted */
public boolean updateItems(long feedId, boolean forceUpdate, int minIntervalMinutes)
{
ArrayList<Feed> feeds = mDB.getFeeds();
Date now = new Date();
ArrayList<Feed> newFeeds = new ArrayList<Feed>();
if( forceUpdate )
{
newFeeds = feeds;
}
else
{
for(Feed feed: feeds)
{
FeedStatus status = mDB.getFeedStatus(feed.mId);
if (status == null)
{
status = new FeedStatus();
status.mFeedId = feed.mId;
}
if( calculateUpdateTime(status, minIntervalMinutes).before(now))
{
newFeeds.add(feed);
}
}
}
if( newFeeds.size() == 0 )
{
return false; // no updates
}
if( mFeedUpdateListener != null )
{
mFeedUpdateListener.feedUpdateProgress(0, newFeeds.size());
}
ArrayList<FeedItem> updatedItems = new ArrayList<FeedItem>();
HttpCache httpCache = new HttpCache(mDB.getContext());
int newItemCount = 0;
int feedNumber = 0;
for(Feed feed: newFeeds)
{
if( feedId != 0 && feed.mId != feedId )
{
continue;
}
FeedStatus status = mDB.getFeedStatus(feed.mId);
if (status == null)
{
status = new FeedStatus();
status.mFeedId = feed.mId;
}
ArrayList<FeedItem> feedItems = new ArrayList<FeedItem>();
ArrayList<Enclosure> enclosures = new ArrayList<Enclosure>();
String oldImageURL = feed.mImageURL;
downloadFeed(feed, status, feedItems, enclosures);
status.mLastHit.setTime( now.getTime() );
mDB.updateFeedStatus(status);
if( feed.mImageURL != null && ! feed.mImageURL.equals(oldImageURL) )
{
boolean theResult = mDB.updateFeedImageURL(feed);
Globals.LOG.d("changing feed image to " + feed.mImageURL + " result " + theResult);
}
updateFeedImage(feed);
ArrayList<ShortFeedItem> dbItems = mDB.getShortFeedItems(feed.mId, null, true);
// search for duplicates, add any new items
for(FeedItem newItem: feedItems)
{
newItem.mFeedId = feed.mId;
boolean needUpdate = false;
boolean found = false;
ShortFeedItem duplicate = findDuplicate(newItem, dbItems);
if( duplicate != null )
{
found = true;
newItem.mId = duplicate.mId;
// if the new item is newer than the newest duplicate
if (newItem.mPubDate.getTime() > duplicate.mPubDate)
{
needUpdate = true;
}
}
if (found && needUpdate)
{
// TODO - why not this
//if (db.updateFeedItem(newItem))
}
else if (!found)
{
if (mLog.d()) mLog.d("hit a new feed item guid " + newItem.mGUID + " link " + newItem.mLink + " inserting into db");
// clean html description
if( newItem.mCleanDescription.length() > 0 )
{
newItem.mCleanDescription = cleanDescription(newItem.mCleanDescription);
}
else
{
newItem.mCleanDescription = cleanDescription(newItem.mDescription);
}
if( newItem.mPubDate.getTime() == 0 )
{
newItem.mPubDate = new Date();
}
newItem.mCleanTitle = cleanDescription(newItem.mTitle);
newItem.mCleanDescription = removeTitleFromDescription( newItem.mCleanDescription, newItem.mCleanTitle );
if (mDB.updateFeedItem(newItem))
{
newItemCount += 1;
dbItems.add(
new ShortFeedItem(
newItem.mId,
newItem.mLink,
newItem.mPubDate.getTime(),
newItem.mTitle,
newItem.mEnclosureURL,
newItem.mGUID,
newItem.mFlags
)
);
if( newItem.mEnclosure != null )
{
newItem.mEnclosure.mItemId = newItem.mId;
mDB.updateEnclosure(newItem.mEnclosure);
}
}
updatedItems.add(newItem);
}
}
if(!feedItems.isEmpty())
{
// find everything in dbitems that is marked as deleted, and check against feed items
// if it's not there, really delete it
for(Iterator<ShortFeedItem> i = dbItems.iterator(); i.hasNext(); )
{
ShortFeedItem item = i.next();
if( (item.mFlags & FeedItem.FLAG_DELETED) != 0 )
{
if ( findDuplicate(item, feedItems) == null )
{
if (mLog.d()) mLog.d("really deleting feed item " + item.mId + " from feed " + item.mFeedId );
mDB.deleteFeedItem(item.mId);
i.remove();
}
}
}
}
else
{
mLog.d("skipping delete items because we didn't get any feed items at all");
}
FeedSettings feedSettings = getFeedSettings(feed.mId);
if( feedSettings != null && feedSettings.mDisplayFullArticle )
{
for(ShortFeedItem feedItem: dbItems)
{
if(
((feedItem.mFlags & FeedItem.FLAG_DELETED) == 0) &&
((feedItem.mFlags & FeedItem.FLAG_READ) == 0)
)
{
httpCache.seed(feedItem.mLink);
}
}
}
if( mFeedUpdateListener != null )
{
mFeedUpdateListener.feedUpdateProgress(feedNumber, newFeeds.size() );
}
feedNumber++;
}
httpCache.maintainCache();
mFeedConfig.addNewItemCount(newItemCount);
// // figure out if there are any images to download
// ArrayList<String> imageURLS = new ArrayList<String>();
// for(FeedItem item: updatedItems)
// {
// if( item.mImageURL.length() > 0 )
// {
// imageURLS.add(item.mImageURL);
// }
// }
// updatedItems.clear();
//
// // then download them
// for(String imageURL: imageURLS)
// {
// if (Globals.LOGGING) Log.d(Globals.LOG_TAG, "downloading an image " + imageURL);
//
// downloadImage(imageURL, false);
//
// if( mFeedUpdateListener != null )
// {
// mFeedUpdateListener.feedUpdateProgress(feedNumber, newFeeds.size() + imageURLS.size() );
// }
// feedNumber++;
// }
//
// // mDB.expireImages();
// for now ignore the above and just delete any existing images, until we figure out how to cache them better
mDB.deleteOlderImages(new Date().getTime());
return true; // updates were tried
}
private void updateFeedImage(Feed feed)
{
if(feed.mImageURL == null || feed.mImageURL.length() == 0)
{
return;
}
Globals.LOG.d("updateFeedImage - downloading image url " + feed.mImageURL + " for feed " + feed.mURL);
downloadImage(feed.mImageURL, true);
}
private ShortFeedItem findDuplicate(FeedItem newItem, ArrayList<ShortFeedItem> items)
{
for(ShortFeedItem item: items)
{
if( newItem.mGUID.length() != 0 && newItem.mGUID.equals(item.mGUID) )
{
if (mLog.v()) mLog.v("direct hit");
// direct hit
return item;
}
// next we check the pub date and title for an exact match
if(
newItem.mPubDate.getTime() == item.mPubDate &&
newItem.mTitle.equals(item.mTitle)
)
{
// fuzzy match but it will do
mLog.d("fuzzy match on title and pub date");
return item;
}
}
return null;
}
private FeedItem findDuplicate(ShortFeedItem newItem, ArrayList<FeedItem> items)
{
for(FeedItem item: items)
{
if( newItem.mGUID.length() != 0 && newItem.mGUID.equals(item.mGUID) )
{
if (mLog.v()) mLog.v("direct hit");
// direct hit
return item;
}
// next we check the pub date and title for an exact match
if(
newItem.mPubDate == item.mPubDate.getTime() &&
newItem.mTitle.equals(item.mTitle)
)
{
// fuzzy match but it will do
mLog.d("fuzzy match on title and pub date");
return item;
}
}
return null;
}
protected Date calculateUpdateTime(FeedStatus feedStatus, int minIntervalMinutes)
{
int ttl = feedStatus.mTTL;
if( ttl > 0)
{
if( ttl < minIntervalMinutes )
{
ttl = minIntervalMinutes;
}
// ignore ttls below 5 minutes
if( ttl < 5 )
{
ttl = 5;
}
// ignore ttls above 1 day
else if( ttl > (24*60) )
{
ttl = 24*60;
}
}
else
{
ttl = 60;
}
if (mLog.d()) mLog.d("set ttl from " + feedStatus.mTTL + " to " + ttl);
Date updateTime = new Date();
updateTime.setTime( feedStatus.mLastHit.getTime() + ttl * 60000);
return updateTime;
}
void downloadImage(String address, boolean persistant)
{
if( mDB.hasImage(address) )
{
mDB.updateImageTime(address, new Date().getTime());
return;
}
try
{
// use apache http client lib to set parameters from feedStatus
DefaultHttpClient client = new DefaultHttpClient();
// set up proxy handler
ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
client.getConnectionManager().getSchemeRegistry(),
ProxySelector.getDefault());
client.setRoutePlanner(routePlanner);
HttpGet request = new HttpGet(address);
request.setHeader("User-Agent", USER_AGENT);
HttpResponse response = client.execute(request);
StatusLine status = response.getStatusLine();
HttpEntity entity = response.getEntity();
if(entity != null && status.getStatusCode() == 200)
{
InputStream inputStream = entity.getContent();
// TODO - parse content-length here
ByteArrayOutputStream data = new ByteArrayOutputStream();
byte bytes[] = new byte[512];
int count;
while( (count = inputStream.read(bytes) ) > 0 )
{
data.write(bytes, 0, count);
}
if( data.size() > 0)
{
mDB.insertImage(address, new Date().getTime(), persistant, data.toByteArray());
}
}
}
catch(IOException exc)
{
if (mLog.e()) mLog.e("error downloading image" + address, exc);
}
}
void downloadFeed(Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures)
{
if( feed.mURL.startsWith("http") )
{
downloadFeedHttp(feed, feedStatus, feedItems, enclosures);
}
}
void downloadFeedHttp(Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures)
{
try
{
// use apache http client lib to set parameters from feedStatus
DefaultHttpClient client = new DefaultHttpClient();
// set up proxy handler
ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
client.getConnectionManager().getSchemeRegistry(),
ProxySelector.getDefault());
client.setRoutePlanner(routePlanner);
HttpGet request = new HttpGet(feed.mURL);
HttpContext httpContext = new BasicHttpContext();
request.setHeader("User-Agent", USER_AGENT);
// send etag if we have it
if (feedStatus.mETag.length() > 0)
{
request.setHeader("If-None-Match", feedStatus.mETag);
}
// send If-Modified-Since if we have it
if (feedStatus.mLastModified.getTime() > 0)
{
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' GMT'", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
String formattedTime = dateFormat.format(feedStatus.mLastModified);
// If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
request.setHeader("If-Modified-Since", formattedTime);
}
request.setHeader("Accept-Encoding", "gzip,deflate");
HttpResponse response = client.execute(request, httpContext);
if (mLog.d()) mLog.d("http request: " + feed.mURL);
if (mLog.d()) mLog.d("http response code: " + response.getStatusLine());
InputStream inputStream = null;
StatusLine status = response.getStatusLine();
HttpEntity entity = response.getEntity();
if(entity != null)
{
inputStream = entity.getContent();
}
try
{
if (entity != null && status.getStatusCode() == 200)
{
Header encodingHeader = entity.getContentEncoding();
if (encodingHeader != null)
{
if (encodingHeader.getValue().equalsIgnoreCase("gzip"))
{
inputStream = new GZIPInputStream(inputStream);
}
else if (encodingHeader.getValue().equalsIgnoreCase("deflate"))
{
inputStream = new InflaterInputStream(inputStream);
}
}
// remove caching attributes to be replaced with new ones
feedStatus.mETag = "";
feedStatus.mLastModified.setTime(0);
feedStatus.mTTL = 0;
boolean success = parseFeed(inputStream, feed, feedStatus, feedItems, enclosures);
if (success)
{
// if the parse was ok, update these attributes
// ETag: "6050003-78e5-4981d775e87c0"
Header etagHeader = response.getFirstHeader("ETag");
if (etagHeader != null)
{
if (etagHeader.getValue().length() < MAX_ETAG_LENGTH)
{
feedStatus.mETag = etagHeader.getValue();
}
else
{
mLog.e("etag length was too big: " + etagHeader.getValue().length());
}
}
// Last-Modified: Fri, 24 Dec 2010 00:57:11 GMT
Header lastModifiedHeader = response.getFirstHeader("Last-Modified");
if (lastModifiedHeader != null)
{
try
{
feedStatus.mLastModified = parseRFC822Date(lastModifiedHeader.getValue());
}
catch(ParseException exc)
{
mLog.e("unable to parse date", exc);
}
}
HttpUriRequest currentReq = (HttpUriRequest) httpContext.getAttribute( ExecutionContext.HTTP_REQUEST );
HttpHost currentHost = (HttpHost) httpContext.getAttribute( ExecutionContext.HTTP_TARGET_HOST );
String currentUrl = currentHost.toURI() + currentReq.getURI();
mLog.w("loaded redirect from " + request.getURI().toString() + " to " + currentUrl);
feedStatus.mLastURL = currentUrl;
}
}
else
{
if (status.getStatusCode() == 304)
{
mLog.d("received 304 not modified");
}
}
}
finally
{
if( inputStream != null)
{
inputStream.close();
}
}
}
catch(IOException exc)
{
mLog.e("error downloading feed " + feed.mURL, exc);
}
}
public static Date parseRFC822Date(String str) throws ParseException
{
for (SimpleDateFormat dateFormat: rfc822DateFormats)
{
try
{
return dateFormat.parse(str);
}
catch(ParseException exc)
{
}
}
throw new ParseException("unable to match any rfc822 date to:" + str, 0);
}
public static Date parseRFC3339Date(String datestring) throws ParseException
{
Date d = new Date();
//if there is no time zone, we don't need to do any special parsing.
if(datestring.endsWith("Z"))
{
try
{
SimpleDateFormat s = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);//spec for RFC3339
d = s.parse(datestring);
}
catch(ParseException pe)
{
//try again with optional decimals
SimpleDateFormat s = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US);//spec for RFC3339 (with fractional seconds)
s.setLenient(true);
d = s.parse(datestring);
}
return d;
}
//step one, split off the timezone.
int zoneMarker = Math.max( datestring.lastIndexOf('-'), datestring.lastIndexOf('+') );
String firstpart = datestring.substring(0,zoneMarker);
String secondpart = datestring.substring(zoneMarker);
//step two, remove the colon from the timezone offset
secondpart = secondpart.substring(0,secondpart.indexOf(':')) + secondpart.substring(secondpart.indexOf(':')+1);
datestring = firstpart + secondpart;
SimpleDateFormat s = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);//spec for RFC3339
try
{
d = s.parse(datestring);
}
catch(java.text.ParseException pe)
{
//try again with optional decimals
s = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ", Locale.US);//spec for RFC3339 (with fractional seconds)
s.setLenient(true);
d = s.parse(datestring);
}
return d;
}
private boolean parseFeed(InputStream is, Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures)
{
try
{
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(is);
parseAsRss(doc, feed, feedStatus, feedItems, enclosures);
parseAsAtom(doc, feed, feedStatus, feedItems, enclosures);
return true;
}
catch (ParserConfigurationException exc)
{
mLog.e("error parsing rss", exc);
}
catch (SAXException exc)
{
mLog.e("error parsing rss", exc);
}
catch (DOMException exc)
{
mLog.e("error parsing rss", exc);
}
catch (IOException exc)
{
mLog.e("error parsing rss", exc);
}
return false;
}
private void parseAsRss(Document doc, Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures)
{
// parse all 'item' elements
NodeList nl = doc.getElementsByTagName("item");
for( int i = 0; i < nl.getLength(); i++)
{
FeedItem feedItem = new FeedItem();
Node node = nl.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE)
{
Element eElement = (Element) node;
feedItem.mTitle = extractValue(eElement, "title");
feedItem.mLink = extractValue(eElement, "link");
feedItem.mGUID = extractValue(eElement, "guid");
feedItem.mAuthor = extractValue(eElement, "author");
feedItem.mDescription = extractValue(eElement, "description");
feedItem.mOriginalLink = extractValue(eElement, "feedburner:origLink");
feedItem.mImageURL = extractAttribute(eElement, "media:thumbnail", "url");
if(feedItem.mAuthor.length() == 0)
{
feedItem.mAuthor = extractValue(eElement, "dc:creator");
}
Date pubDate = new Date();
pubDate.setTime(0);
String pubDateString = extractValue(eElement, "pubDate");
try
{
pubDate = parseRFC822Date(pubDateString);
}
catch(ParseException exc)
{
mLog.e("unable to parse item pubdate:" + pubDateString);
}
feedItem.mPubDate = pubDate;
NodeList enclosuresList = eElement.getElementsByTagName("enclosure");
if( enclosuresList != null && enclosuresList.getLength() > 0 )
{
NamedNodeMap enclosureAttributes = enclosuresList.item(0).getAttributes();
if( enclosureAttributes != null )
{
Enclosure enclosure = new Enclosure();
Node enclosureURL = enclosureAttributes.getNamedItem("url");
Node enclosureLength = enclosureAttributes.getNamedItem("length");
Node enclosureType = enclosureAttributes.getNamedItem("type");
if( enclosureURL != null )
{
enclosure.mURL = enclosureURL.getNodeValue();
}
if( enclosureLength != null )
{
try
{
enclosure.mLength = Long.parseLong( enclosureLength.getNodeValue() );
}
catch( NumberFormatException exc )
{
mLog.e("error parsing enclosure length", exc);
}
}
if( enclosureType != null )
{
enclosure.mContentType = enclosureType.getNodeValue();
}
String duration = extractValue(eElement, "itunes:duration");
if( duration != null && duration.length() > 0 )
{
enclosure.mDuration = Utilities.parseDuration(duration) * 1000;
}
duration = extractAttribute(eElement, "media:content", "duration");
if( duration != null && duration.length() > 0 )
{
enclosure.mDuration = Utilities.parseDuration(duration) * 1000;
}
duration = extractValue(eElement, "blip:runtime");
if( duration != null && duration.length() > 0 )
{
enclosure.mDuration = Utilities.parseDuration(duration) * 1000;
}
// TODO - find a better way to do this
// nuke image enclosures for now
if( enclosure.mContentType.startsWith("image/") )
{
enclosure.mURL = "";
}
if( enclosure.mURL.length() > 0 )
{
try
{
URL url = new URL(enclosure.mURL);
feedItem.mEnclosureURL = url.toExternalForm();
enclosure.mURL = url.toExternalForm();
feedItem.mEnclosure = enclosure;
enclosures.add( enclosure );
}
catch( MalformedURLException exc )
{
mLog.e("error parsing enclosure url", exc);
}
}
}
}
feedItems.add( feedItem );
}
}
// parse all 'channel' elements
nl = doc.getElementsByTagName("channel");
for( int i = 0; i < nl.getLength(); i++)
{
Node node = nl.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE)
{
// extract ttl
Element eElement = (Element) node;
String ttlString = extractValue( eElement, "ttl");
if( ttlString.length() > 0)
{
try
{
feedStatus.mTTL = Integer.parseInt(ttlString);
}
catch (NumberFormatException exc)
{
mLog.e("error parsing ttl: " + ttlString, exc);
}
}
if( feed != null )
{
String title = extractValue( eElement, "title");
String link = extractValue( eElement, "link");
String description = extractValue( eElement, "description");
feed.mName = title;
feed.mLink = link;
feed.mDescription = description;
Node imageNode = eElement.getElementsByTagName("image").item(0);
if(imageNode != null && imageNode.getNodeType() == Node.ELEMENT_NODE)
{
String imageUrl = extractValue((Element)imageNode, "url");
feed.mImageURL = imageUrl;
}
}
}
}
}
private void parseAsAtom(Document doc, Feed feed, FeedStatus feedStatus, ArrayList<FeedItem> feedItems, ArrayList<Enclosure> enclosures)
{
// parse all 'item' elements
NodeList nl = doc.getElementsByTagName("entry");
for( int i = 0; i < nl.getLength(); i++)
{
FeedItem feedItem = new FeedItem();
Node node = nl.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE)
{
Element eElement = (Element) node;
feedItem.mTitle = extractValue(eElement, "title");
feedItem.mGUID = extractValue(eElement, "id");
feedItem.mCleanDescription = extractValue(eElement, "summary");
feedItem.mDescription = extractValue(eElement, "content");
feedItem.mOriginalLink = extractValue(eElement, "feedburner:origLink");
feedItem.mImageURL = extractAttribute(eElement, "media:thumbnail", "url");
NodeList authorNodes = eElement.getElementsByTagName("author");
for(int j = 0; j < authorNodes.getLength(); ++j )
{
if( authorNodes.item(j).getNodeType() == Node.ELEMENT_NODE )
{
feedItem.mAuthor = extractValue((Element)(authorNodes.item(j)), "name");
if( feedItem.mAuthor.length() > 0 )
{
break;
}
}
}
Date pubDate = new Date();
pubDate.setTime(0);
String pubDateString = extractValue(eElement, "updated");
try
{
pubDate = parseRFC3339Date(pubDateString);
}
catch(ParseException exc)
{
mLog.e("unable to parse item pubdate:" + pubDateString);
}
feedItem.mPubDate = pubDate;
NodeList linksList = eElement.getElementsByTagName("link");
if( linksList != null )
{
for(int j = 0; j < linksList.getLength(); ++j)
{
NamedNodeMap linkAttributes = linksList.item(j).getAttributes();
Node relNode = linkAttributes.getNamedItem("rel");
if( relNode == null )
{
continue;
}
String rel = relNode.getNodeValue();
if( rel.equals("alternate") && feedItem.mLink.length() == 0 )
{
feedItem.mLink = extractAttribute(eElement, "link", "href");
}
else if( rel.equals("enclosure") )
{
Enclosure enclosure = new Enclosure();
Node enclosureURL = linkAttributes.getNamedItem("href");
Node enclosureLength = linkAttributes.getNamedItem("length");
Node enclosureType = linkAttributes.getNamedItem("type");
if( enclosureURL != null )
{
enclosure.mURL = enclosureURL.getNodeValue();
}
if( enclosureLength != null )
{
try
{
enclosure.mLength = Long.parseLong( enclosureLength.getNodeValue() );
}
catch( NumberFormatException exc )
{
mLog.e("error parsing enclosure length", exc);
}
}
if( enclosureType != null )
{
enclosure.mContentType = enclosureType.getNodeValue();
}
// TODO - find a better way to do this
// nuke image enclosures for now
if( enclosure.mContentType.startsWith("image/") )
{
enclosure.mURL = "";
}
if( enclosure.mURL.length() > 0 )
{
try
{
URL url = new URL(enclosure.mURL);
feedItem.mEnclosureURL = url.toExternalForm();
enclosure.mURL = url.toExternalForm();
feedItem.mEnclosure = enclosure;
enclosures.add( enclosure );
}
catch( MalformedURLException exc )
{
mLog.e("error parsing enclosure url", exc);
}
}
}
}
}
if( feedItem.mLink.length() > 0 )
{
feedItems.add( feedItem );
}
}
} // proccess all entries
// parse all 'feed' elements
nl = doc.getElementsByTagName("feed");
for( int i = 0; i < nl.getLength(); i++)
{
Node node = nl.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE)
{
// extract ttl
Element eElement = (Element) node;
if( feed != null )
{
String title = extractValue( eElement, "title");
String link = extractAttribute(eElement, "link", "href");
String description = extractValue( eElement, "subtitle");
feed.mName = title;
feed.mLink = link;
feed.mDescription = description;
feed.mImageURL = extractValue( eElement, "icon");
}
}
feedStatus.mTTL = 10;
}
}
private String extractValue(Element eElement, String tag)
{
StringBuilder builder = new StringBuilder();
NodeList nlList = eElement.getElementsByTagName(tag);
if (nlList.getLength() > 0)
{
nlList = nlList.item(0).getChildNodes();
for( int j = 0; j < nlList.getLength(); ++j)
{
Node nValue = (Node) nlList.item(j);
builder.append( nValue.getNodeValue() );
}
deSpace(builder);
}
return builder.toString();
}
private String extractAttribute(Element root, String tagName, String attributeName)
{
StringBuffer buffer = new StringBuffer();
NodeList nlList = root.getElementsByTagName(tagName);
if (nlList.getLength() > 0)
{
NamedNodeMap attributes = nlList.item(0).getAttributes();
Node nValue = attributes.getNamedItem(attributeName);
if( nValue != null )
{
buffer.append(nValue.getNodeValue());
}
}
return buffer.toString();
}
private void deSpace(StringBuilder builder)
{
// remove whitespace from front
while ( builder.length() > 0 )
{
if (Character.isWhitespace(builder.charAt(0)))
{
builder.deleteCharAt(0);
}
else
{
break;
}
}
// remove whitespace from back
while (builder.length() > 0)
{
int j = builder.length() - 1;
if( Character.isWhitespace(builder.charAt(j)))
{
builder.deleteCharAt(j);
}
else
{
break;
}
}
// remove duplicate spaces
// TODO - detect any whitespace type
boolean previousWasSpace = false;
for( int j = 0; j < builder.length();)
{
char c = builder.charAt(j);
if( previousWasSpace && c == ' ' )
{
builder.deleteCharAt(j);
}
else
{
++j;
previousWasSpace = c == ' ';
}
}
}
public String cleanDescription(TagNode node)
{
final StringBuilder description = new StringBuilder();
node.traverse(new TagNodeVisitor()
{
@Override
public boolean visit(TagNode tagNode, HtmlNode htmlNode)
{
if(htmlNode instanceof ContentNode)
{
ContentNode contentNode = (ContentNode) htmlNode;
htmlUnescapeInto( contentNode.getContent(), description );
}
return true;
}
}
);
return description.toString().trim();
}
public String cleanDescription(String input)
{
HtmlCleaner cleaner = new HtmlCleaner();
CleanerProperties props = cleaner.getProperties();
props.setOmitComments(true);
TagNode node = cleaner.clean(input);
return cleanDescription(node);
}
public String removeTitleFromDescription(String description, String title)
{
if( description.startsWith( title ) )
{
description = description.substring( title.length() , description.length() ).trim();
}
return description;
}
public void loadConfig()
{
mPreviousPackageVersion = mFeedConfig.getPreviousPackageVersion(mPreviousPackageVersion);
}
public void setFeedUpdateListener(FeedUpdateListener listener)
{
mFeedUpdateListener = listener;
}
public boolean isFirstRun()
{
return mPreviousPackageVersion <= Globals.PREVIOUS_VERSION_CODE;
}
public void clearFirstRun()
{
if( mPreviousPackageVersion != mPackageVersion )
{
mPreviousPackageVersion = mPackageVersion;
mFeedConfig.setPreviousPackageVersion( mPreviousPackageVersion );
}
}
public byte[] getImage(String imageURL)
{
return mDB.getImage(imageURL);
}
public Feed getFeed(long feedId)
{
return mDB.getFeed(feedId);
}
public Feed getFeedByItemId(long itemId)
{
return mDB.getFeedByItemId( itemId );
}
public void htmlUnescapeInto(StringBuilder source, StringBuilder dest )
{
boolean inEntity = false;
StringBuilder entity = new StringBuilder(10);
int sourceLength = source.length();
for( int i = 0; i < sourceLength; ++i)
{
char c = source.charAt(i);
if( inEntity )
{
if( c == ';' )
{
// first see if this is a special sequence
SpecialEntity special = org.htmlcleaner.SpecialEntity.getEntity(entity.toString());
if( special != null )
{
dest.append( special.getCharacter() );
}
// next try hex starting with #x
else if( entity.length() > 1 && entity.charAt(0) == '#' && entity.charAt(1) == 'x')
{
int value = 0;
for( int j = 2; j < entity.length(); ++j )
{
value *= 16;
char e = entity.charAt(j);
if( e >= '0' && e <= '9')
{
value += (int) (e - '0');
}
else if( e >= 'a' && e <= 'f')
{
value += 10 + (int) (e - 'a');
}
else if ( e >= 'A' && e <= 'F')
{
value += 10 + (int) (e - 'A');
}
else
{
value = 0;
break;
}
}
if( value != 0 )
{
dest.append((char) value);
}
}
// next try decimal starting with #
else if( entity.length() > 0 && entity.charAt(0) == '#' )
{
int value = 0;
for( int j = 1; j < entity.length(); ++j )
{
value *= 10;
char e = entity.charAt(j);
if( e >= '0' && e <= '9')
{
value += (int) (e - '0');
}
else
{
value = 0;
break;
}
}
if( value != 0 )
{
dest.append((char) value);
}
}
inEntity = false;
entity.setLength(0);
}
else
{
entity.append(c);
}
}
else
{
if( c == '&' )
{
inEntity = true;
}
else
{
dest.append(c);
}
}
}
}
public boolean addFeed(URL url, String name, int feedType)
{
boolean success = false;
Feed feed = new Feed(feedType);
feed.mURL = url.toExternalForm();
FeedStatus status = new FeedStatus();
ArrayList<FeedItem> items = new ArrayList<FeedItem>();
ArrayList<Enclosure> enclosures = new ArrayList<Enclosure>();
// TODO - this should be merged with the feed updater
// TODO - this should actually check for success
downloadFeed(feed, status, items, enclosures);
if( name != null )
{
feed.mName = name;
}
if( items.size() > 0 && feed.mName.length() > 0 )
{
mLog.w("feed downloaded, adding to db");
if ( mDB.addFeed(feed) )
{
status.mFeedId = feed.mId;
mDB.updateFeedStatus( status );
mLog.d("status updated, adding " + items.size() + " items");
for( FeedItem item: items )
{
item.mFeedId = feed.mId;
if( item.mCleanDescription.length() > 0 )
{
item.mCleanDescription = cleanDescription(item.mCleanDescription);
}
else
{
item.mCleanDescription = cleanDescription(item.mDescription);
}
item.mCleanTitle = cleanDescription(item.mTitle);
item.mCleanDescription = removeTitleFromDescription( item.mCleanDescription, item.mCleanTitle );
if(mLog.d()) mLog.d("adding item " + item.mCleanTitle + " enclosure " + item.mEnclosureURL );
mDB.updateFeedItem( item );
if( item.mEnclosure != null )
{
item.mEnclosure.mItemId = item.mId;
}
}
for( Enclosure enclosure: enclosures )
{
mDB.updateEnclosure(enclosure);
}
updateFeedImage(feed);
success = true;
}
}
return success;
}
public FeedDBAdaptor getDBAdaptor()
{
return mDB;
}
public Enclosure getEnclosure(String url)
{
return mDB.getEnclosure(url);
}
public Enclosure getEnclosure(FeedItem item)
{
Enclosure enclosure = mDB.getEnclosureFromItemId(item.mId);
if( enclosure != null )
{
item.mEnclosure = enclosure;
}
return enclosure;
}
public boolean createFile (Enclosure enclosure)
{
try
{
// track down the feed it belongs to
String feedName = null;
FeedItem item = mDB.getFeedItem( enclosure.mItemId );
if( item != null )
{
Feed feed = mDB.getFeed( item.mFeedId );
if( feed != null)
{
feedName = feed.mName;
}
}
if( feedName == null )
{
mLog.e("unabled to find feed for enclosure " + enclosure.mURL );
return false;
}
URL url = new URL( enclosure.mURL );
String[] parts = url.getPath().split("/");
String filename = url.getPath();
if( parts.length > 0 )
{
filename = parts[ parts.length - 1 ];
}
// TODO - sanitise file name
String destinationPath = Environment.getExternalStorageDirectory().getAbsolutePath() +
File.separator + "Podcasts" + File.separator + feedName + File.separator + filename;
File destination = new File(destinationPath);
for(int i = 1; i <= 100; ++i)
{
if( destination.exists() )
{
destination = new File( destinationPath + "." + i );
}
else
{
break;
}
}
mLog.e("downloading " + url.toExternalForm() + " to " + destination);
File parentFile = destination.getParentFile();
if( ! parentFile.isDirectory() )
{
parentFile.mkdirs();
mLog.e("Tried to create parent directory " + parentFile);
}
boolean success = false;
try
{
success = destination.createNewFile();
}
catch( IOException exc )
{
mLog.e("Error creating destination file " + destination + " for download", exc);
}
if( success )
{
enclosure.mDownloadPath = destination.getAbsolutePath();
}
return success;
}
catch( MalformedURLException exc )
{
mLog.e("beginDownload - error parsing url", exc);
}
return false;
}
public Enclosure getEnclosure(long enclosureId)
{
return mDB.getEnclosure(enclosureId);
}
public boolean updateEnclosure(Enclosure enclosure)
{
return mDB.updateEnclosure(enclosure);
}
public HashMap<Long, FeedEnclosureInfo> getFeedEnclosureInfo(String enclosureType)
{
return mDB.getFeedEnclosureInfo(enclosureType);
}
public HashMap<Long, FeedEnclosureInfo> getFeedsWithoutEnclosuresInfo()
{
return mDB.getFeedsWithoutEnclosuresInfo();
}
public ArrayList<Feed> getFeeds()
{
return mDB.getFeeds();
}
public ArrayList<FeedItemEnclosureInfo> getFeedItemEnclosureInfo(long feedId, String enclosureType)
{
return mDB.getFeedItemEnclosureInfo(feedId, enclosureType);
}
@SuppressWarnings("unchecked")
public synchronized ArrayList<Download> getDownloads()
{
return (ArrayList<Download>) mDownloads.clone();
}
public synchronized boolean isDownloading(Enclosure enclosure)
{
for(Download download: mDownloads)
{
if( download.mEnclosureId == enclosure.mId )
{
return true;
}
}
return false;
}
public synchronized void addDownload(FeedItem item, Enclosure enclosure)
{
if( isDownloading( enclosure ) )
{
return;
}
Download download = new Download();
download.mId = mDB.addDownload( enclosure.mId );
download.mEnclosureId = enclosure.mId;
download.mName = item.mCleanTitle;
download.mSize = enclosure.mLength;
download.mDownloaded = 0;
download.mInProgress = false;
download.mCancelled = false;
mDownloads.add( download );
}
private synchronized void loadDownloads()
{
ArrayList<Download> downloads = mDB.getAllDownloads();
for(Download download: downloads)
{
Enclosure enclosure = mDB.getEnclosure(download.mEnclosureId);
if( enclosure == null)
{
continue;
}
FeedItem item = mDB.getFeedItem(enclosure.mItemId);
if( item == null)
{
continue;
}
download.mName = item.mCleanTitle;
download.mSize = enclosure.mLength;
download.mDownloaded = 0;
download.mInProgress = false;
mDownloads.add( download );
}
}
public synchronized void downloadComplete(Download download)
{
mDB.deleteDownload(download.mId);
mDownloads.remove(download);
}
public synchronized boolean deleteDownload(Download download, Enclosure enclosure)
{
File f = new File( enclosure.mDownloadPath );
boolean deleted = f.delete();
mDB.deleteDownload(download.mId);
mDownloads.remove(download);
return deleted;
}
public void deleteFeed(Feed feed, boolean deleteDownloads)
{
if(feed == null) {
return;
}
ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feed.mId, null, true);
for(ShortFeedItem item: items)
{
Enclosure enclosure = mDB.getEnclosureFromItemId(item.mId);
if( enclosure == null )
{
continue;
}
for(Download download: mDownloads)
{
if( download.mEnclosureId == enclosure.mId )
{
download.mCancelled = true;
}
}
if( enclosure.mDownloadPath.length() > 0 && deleteDownloads )
{
File f = new File( enclosure.mDownloadPath );
boolean deleted = f.delete();
if( !deleted ) mLog.e("failed to delete downloaded enclosure " + enclosure.mDownloadPath);
// attempt to delete enclosing folder - don't care about result
f.getParentFile().delete();
}
mDB.deleteDownloadByEnclosure(enclosure.mId);
mDB.deleteEnclosure(enclosure.mId);
}
mDB.deleteFeed( feed.mId );
}
public boolean deleteFeedItem(FeedItem item)
{
item.mFlags = item.mFlags | FeedItem.FLAG_DELETED;
item.mDescription = "";
item.mCleanDescription = "";
boolean success = mDB.updateFeedItem(item);
Enclosure enclosure = getEnclosure(item);
if( enclosure != null )
{
mDB.deleteEnclosure(enclosure.mId);
mDB.deleteDownloadByEnclosure(enclosure.mId);
if( enclosure.mDownloadPath.length() > 0 )
{
File f = new File( enclosure.mDownloadPath );
boolean deleted = f.delete();
if( !deleted ) mLog.e("failed to delete downloaded enclosure " + enclosure.mDownloadPath);
}
}
if( !success ) mLog.e("failed to delete item id " + item.mId );
return success;
}
public void deleteDownloadedEnclosure(Enclosure enclosure)
{
mDB.deleteDownloadByEnclosure(enclosure.mId);
if( enclosure.mDownloadPath.length() > 0 )
{
File f = new File( enclosure.mDownloadPath );
boolean deleted = f.delete();
if( !deleted ) mLog.e("failed to delete downloaded enclosure " + enclosure.mDownloadPath);
}
enclosure.mDownloadPath = "";
enclosure.mDownloadTime = 0;
mDB.updateEnclosure(enclosure);
}
public void deleteFeedItemsRead(long feedId)
{
long startTime = System.currentTimeMillis();
ArrayList<ShortFeedItem> items = mDB.getShortFeedItems(feedId, null, false);
mDB.mDb.beginTransaction();
try
{
for(ShortFeedItem item: items)
{
if (
(item.mFlags & FeedItem.FLAG_DELETED) == 0 &&
(item.mFlags & FeedItem.FLAG_READ) != 0 &&
(item.mFlags & FeedItem.FLAG_STARRED) == 0
)
{
FeedItem fullItem = getItemById(item.mId);
deleteFeedItem(fullItem);
}
}
mDB.mDb.setTransactionSuccessful();
}
finally
{
mDB.mDb.endTransaction();
}
if(mLog.d()) mLog.d("deleteFeedItemsRead " + items.size() + " took " + (System.currentTimeMillis() - startTime));
}
public void setFeedItemsRead(long feedId)
{
mDB.setFeedItemsRead(feedId);
}
public void verifyEnclosure(Enclosure enclosure)
{
boolean changed = false;
if( enclosure.mDownloadTime > 0 )
{
File f = new File( enclosure.mDownloadPath );
if( ! f.exists() )
{
enclosure.mDownloadTime = 0;
changed = true;
}
}
if( changed )
{
updateEnclosure(enclosure);
}
}
public FeedConfig getFeedConfig()
{
return mFeedConfig;
}
public Feed getLocalFeed()
{
Feed localFeed = mDB.getFeedByURL(Feed.SCHEME_LOCAL + ":/readlater");
if( localFeed == null )
{
localFeed = new Feed(Feed.TYPE_NEWS);
localFeed.mName = "Local Bookmarks";
localFeed.mURL = Feed.SCHEME_LOCAL + ":/readlater";
if( mDB.addFeed(localFeed) )
{
FeedSettings feedSettings = new FeedSettings();
feedSettings.mFeedId = localFeed.mId;
feedSettings.mCacheFullArticle = true;
feedSettings.mDisplayFullArticle = true;
feedSettings.mCacheImages = true;
feedSettings.mTextify = true;
feedSettings.mUpdateAutomatically = true;
mDB.updateFeedSettings(feedSettings);
}
}
return localFeed;
}
public void addLocalBookmark(String url)
{
Feed localFeed = getLocalFeed();
FeedItem item = new FeedItem();
item.mFeedId = localFeed.mId;
item.mLink = url;
item.mOriginalLink = url;
item.mPubDate = new Date();
item.mCleanTitle = url;
item.mTitle = url;
// TODO - add this article to the download queue and flesh out some details
mDB.updateFeedItem(item);
}
public FeedSettings getFeedSettings(long feedId)
{
FeedSettings feedSettings = mDB.getFeedSettings(feedId);
if( feedSettings == null )
{
feedSettings = new FeedSettings();
feedSettings.mFeedId = feedId;
feedSettings.mCacheFullArticle = true;
feedSettings.mDisplayFullArticle = false;
feedSettings.mCacheImages = true;
feedSettings.mTextify = false;
feedSettings.mUpdateAutomatically = true;
}
return feedSettings;
}
public void updateFeedSettings(FeedSettings mFeedSettings)
{
mDB.updateFeedSettings(mFeedSettings);
}
public String exportOPML()
{
try
{
ByteArrayOutputStream os = new ByteArrayOutputStream(16 * 1024);
XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance();
XmlSerializer serializer = parserFactory.newSerializer();
serializer.setOutput(os, "UTF-8");
serializer.startDocument("UTF-8", true);
serializer.startTag(null, "opml");
serializer.attribute(null, "version", "1.0");
serializer.startTag(null, "head");
serializer.startTag(null, "title");
serializer.text("FeedScribe Export");
serializer.endTag(null, "title");
serializer.endTag(null, "head");
serializer.startTag(null, "body");
ArrayList<Feed> feeds = mDB.getFeeds();
for(Feed feed: feeds)
{
serializer.startTag(null, "outline");
serializer.attribute(null, "text", feed.mName);
serializer.attribute(null, "title", feed.mName);
serializer.attribute(null, "type", "rss");
serializer.attribute(null, "xmlUrl", feed.mURL);
if( feed.mLink != null && feed.mLink.length() > 0 )
{
serializer.attribute(null, "htmlUrl", feed.mLink);
}
serializer.endTag(null, "outline");
}
serializer.endTag(null, "body");
serializer.endTag(null, "opml");
serializer.endDocument();
return os.toString();
}
catch(IOException exc)
{
mLog.e("FeedManager.exportOPML", exc);
}
catch(XmlPullParserException exc)
{
mLog.e("FeedManager.exportOPML", exc);
}
return null;
}
/**
* @return -1 for failure, otherwise number of items imported
*/
public int importOPML(String data)
{
try
{
XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance();
XmlPullParser parser = parserFactory.newPullParser();
parser.setInput(new StringReader(data) );
return importOPML( parser );
}
catch (XmlPullParserException e)
{
mLog.e("importOPML", e);
}
return -1;
}
/**
* @return -1 for failure, otherwise number of items imported
*/
public int importOPML(Reader reader)
{
try
{
XmlPullParserFactory parserFactory = XmlPullParserFactory.newInstance();
XmlPullParser parser = parserFactory.newPullParser();
parser.setInput( reader );
return importOPML( parser );
}
catch (XmlPullParserException e)
{
mLog.e("importOPML", e);
}
return -1;
}
/**
*
* @param parser
* @return -1 for failure, otherwise number of items imported
*/
protected int importOPML(XmlPullParser parser)
{
try
{
int eventType = parser.getEventType();
boolean isOPML = false;
boolean inBody = false;
int numAdded = 0;
while( eventType != XmlPullParser.END_DOCUMENT )
{
if( eventType == XmlPullParser.START_DOCUMENT )
{
}
else if( eventType == XmlPullParser.START_TAG)
{
String tag = parser.getName();
if( "opml".equals(tag) && "1.0".equals(parser.getAttributeValue(null, "version")) )
{
isOPML = true;
}
else if( "body".equals(tag) && isOPML )
{
inBody = true;
}
else if( "outline".equals(tag) && inBody && "rss".equals(parser.getAttributeValue(null, "type")) )
{
String name = parser.getAttributeValue(null, "title");
if( name == null )
{
name = parser.getAttributeValue(null, "text");
}
String url = parser.getAttributeValue(null, "xmlUrl");
if( name != null && url != null )
{
url = url.toLowerCase(Locale.US);
mLog.w("checking for existing feed name " + name + " url " + url);
// make sure we don't duplicate any urls
boolean found = false;
ArrayList<Feed> feeds = getFeeds();
for(Feed feed: feeds)
{
if( feed.mURL.equals(url) )
{
found = true;
}
}
if(!found)
{
try
{
mLog.w("adding feed name " + name + " url " + url);
URL realURL = new URL(url);
addFeed(realURL, name, Feed.TYPE_PODCAST);
numAdded += 1;
}
catch(MalformedURLException exc)
{
mLog.e("importOPML", exc);
}
}
}
}
}
else if( eventType == XmlPullParser.END_TAG)
{
String tag = parser.getName();
if( "body".equals(tag) )
{
inBody = false;
}
}
eventType = parser.next();
}
if(isOPML)
{
return numAdded;
}
else
{
return -1;
}
}
catch (XmlPullParserException e)
{
mLog.e("importOPML", e);
}
catch (IOException e)
{
mLog.e("importOPML", e);
}
return -1;
}
public boolean setFeedName(long feedId, String newName)
{
return mDB.setFeedName(feedId, newName);
}
}